/*
 * @(#)QuickTimeMovieMakerMain.java  1.0  2010-10-02
 * 
 * Copyright © 2010 Werner Randelshofer
 * Staldenmattweg 2, Immensee, CH-6405, Switzerland.
 * All rights reserved.
 * 
 * The copyright of this software is owned by Werner Randelshofer. 
 * You may not use, copy or modify this software, except in  
 * accordance with the license agreement you entered into with  
 * Werner Randelshofer. For details see accompanying license terms.
 */
package ch.randelshofer.media.quicktime;

import ch.randelshofer.gui.datatransfer.FileTextFieldTransferHandler;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.prefs.Preferences;
import javax.imageio.ImageIO;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.ProgressMonitor;
import javax.swing.SwingWorker;
import javax.swing.border.EmptyBorder;

/**
 * A demo for the {@link QuickTimeWriter} class.
 *
 * @author Werner Randelshofer
 * @version 1.0 2010-10-02 Created.
 */
public class QuickTimeMovieMakerMain extends javax.swing.JFrame {

    private JFileChooser imageFolderChooser;
    private JFileChooser soundFileChooser;
    private JFileChooser movieFileChooser;
    private Preferences prefs;

    /** Creates new form QuickTimeMovieMakerMain */
    public QuickTimeMovieMakerMain() {
        initComponents();
        ((JComponent) getContentPane()).setBorder(new EmptyBorder(12, 18, 18, 18));
        imageFolderField.setTransferHandler(new FileTextFieldTransferHandler(JFileChooser.DIRECTORIES_ONLY));
        soundFileField.setTransferHandler(new FileTextFieldTransferHandler());

        JComponent[] smallComponents = {encodingBox,
            encodingLabel,
            fpsField,
            fpsLabel,
            heightField,
            heightLabel,
            widthField,
            widthLabel,
            passThroughCheckBox};
        for (JComponent c : smallComponents) {
            c.putClientProperty("JComponent.sizeVariant", "small");
        }

        // Get Preferences
        prefs = Preferences.userNodeForPackage(QuickTimeMovieMakerMain.class);
        imageFolderField.setText(prefs.get("movie.imageFolder", ""));
        soundFileField.setText(prefs.get("movie.soundFile", ""));
        widthField.setText("" + prefs.getInt("movie.width", 320));
        heightField.setText("" + prefs.getInt("movie.height", 240));
        passThroughCheckBox.setSelected(prefs.getBoolean("movie.passThrough", false));
        String fps = "" + prefs.getDouble("movie.fps", 30);
        if (fps.endsWith(".0")) {
            fps = fps.substring(0, fps.length() - 2);
        }
        fpsField.setText(fps);
        encodingBox.setSelectedIndex(Math.max(0, Math.min(encodingBox.getItemCount() - 1, prefs.getInt("movie.encoding", 1))));

        //
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        aboutLabel = new javax.swing.JLabel();
        imageFolderHelpLabel = new javax.swing.JLabel();
        imageFolderField = new javax.swing.JTextField();
        chooseImageFolderButton = new javax.swing.JButton();
        soundFileHelpLable = new javax.swing.JLabel();
        soundFileField = new javax.swing.JTextField();
        chooseSoundFileButton = new javax.swing.JButton();
        createMovieButton = new javax.swing.JButton();
        widthLabel = new javax.swing.JLabel();
        widthField = new javax.swing.JTextField();
        heightLabel = new javax.swing.JLabel();
        heightField = new javax.swing.JTextField();
        encodingLabel = new javax.swing.JLabel();
        encodingBox = new javax.swing.JComboBox();
        fpsLabel = new javax.swing.JLabel();
        fpsField = new javax.swing.JTextField();
        passThroughCheckBox = new javax.swing.JCheckBox();

        FormListener formListener = new FormListener();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("QuickTime Movie Maker");

        aboutLabel.setText("<html><b>This is a demo for the QuickTimeWriter class.</b><br>\nQuickTimeWriter supports writing of audio and video data in a QuickTime Movie file using pure Java.<br>\nCopyright © 2010 Werner Randelshofer. All rights reserved.<br>\nThis software can be licensed under Creative Commons Atribution 3.0.");

        imageFolderHelpLabel.setText("Drag a folder with image files into the field below:");

        chooseImageFolderButton.setText("Choose...");
        chooseImageFolderButton.addActionListener(formListener);

        soundFileHelpLable.setText("Drag a sound file into the field below:");

        chooseSoundFileButton.setText("Choose...");
        chooseSoundFileButton.addActionListener(formListener);

        createMovieButton.setText("Create QuickTime Movie...");
        createMovieButton.addActionListener(formListener);

        widthLabel.setFont(new java.awt.Font("Lucida Grande", 0, 11));
        widthLabel.setText("Width:");

        widthField.setColumns(4);
        widthField.setFont(new java.awt.Font("Lucida Grande", 0, 11));
        widthField.setText("320");

        heightLabel.setFont(new java.awt.Font("Lucida Grande", 0, 11));
        heightLabel.setText("Height:");

        heightField.setColumns(4);
        heightField.setFont(new java.awt.Font("Lucida Grande", 0, 11));
        heightField.setText("240");

        encodingLabel.setFont(new java.awt.Font("Lucida Grande", 0, 11));
        encodingLabel.setText("Encoding:");

        encodingBox.setFont(new java.awt.Font("Lucida Grande", 0, 11));
        encodingBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "JPEG", "PNG", "Raw" }));
        encodingBox.setSelectedIndex(1);

        fpsLabel.setFont(new java.awt.Font("Lucida Grande", 0, 11));
        fpsLabel.setText("FPS:");

        fpsField.setColumns(4);
        fpsField.setFont(new java.awt.Font("Lucida Grande", 0, 11));
        fpsField.setText("30");

        passThroughCheckBox.setFont(new java.awt.Font("Lucida Grande", 0, 11)); // NOI18N
        passThroughCheckBox.setText("Pass through");
        passThroughCheckBox.setToolTipText("Check this box if the folder contains already encoded video frames in the desired size.");

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                        .addGap(61, 61, 61)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(widthLabel)
                            .addComponent(fpsLabel))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(fpsField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                                .addComponent(encodingLabel)
                                .addGap(1, 1, 1)
                                .addComponent(encodingBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                                .addComponent(passThroughCheckBox))
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(widthField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                                .addComponent(heightLabel)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(heightField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
                        .addGap(41, 41, 41))
                    .addGroup(layout.createSequentialGroup()
                        .addContainerGap()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(aboutLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 441, Short.MAX_VALUE)
                            .addComponent(imageFolderHelpLabel)
                            .addComponent(soundFileHelpLable)
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(soundFileField, javax.swing.GroupLayout.DEFAULT_SIZE, 329, Short.MAX_VALUE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(chooseSoundFileButton))
                            .addComponent(createMovieButton, javax.swing.GroupLayout.Alignment.TRAILING)
                            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                                .addComponent(imageFolderField, javax.swing.GroupLayout.DEFAULT_SIZE, 329, Short.MAX_VALUE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(chooseImageFolderButton)))))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(aboutLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addGap(18, 18, 18)
                .addComponent(imageFolderHelpLabel)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(imageFolderField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(chooseImageFolderButton))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(widthLabel)
                    .addComponent(widthField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(heightLabel)
                    .addComponent(heightField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(encodingBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(fpsLabel)
                    .addComponent(fpsField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(encodingLabel)
                    .addComponent(passThroughCheckBox))
                .addGap(18, 18, 18)
                .addComponent(soundFileHelpLable)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(soundFileField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(chooseSoundFileButton))
                .addGap(18, 18, Short.MAX_VALUE)
                .addComponent(createMovieButton)
                .addContainerGap())
        );

        pack();
    }

    // Code for dispatching events from components to event handlers.

    private class FormListener implements java.awt.event.ActionListener {
        FormListener() {}
        public void actionPerformed(java.awt.event.ActionEvent evt) {
            if (evt.getSource() == chooseImageFolderButton) {
                QuickTimeMovieMakerMain.this.chooseImageFolder(evt);
            }
            else if (evt.getSource() == chooseSoundFileButton) {
                QuickTimeMovieMakerMain.this.chooseSoundFile(evt);
            }
            else if (evt.getSource() == createMovieButton) {
                QuickTimeMovieMakerMain.this.createMovie(evt);
            }
        }
    }// </editor-fold>//GEN-END:initComponents

    private void chooseImageFolder(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseImageFolder
        if (imageFolderChooser == null) {
            imageFolderChooser = new JFileChooser();
            imageFolderChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
            if (imageFolderField.getText().length() > 0) {
                imageFolderChooser.setSelectedFile(new File(imageFolderField.getText()));
            }
        }
        if (JFileChooser.APPROVE_OPTION == imageFolderChooser.showOpenDialog(this)) {
            imageFolderField.setText(imageFolderChooser.getSelectedFile().getPath());
        }
    }//GEN-LAST:event_chooseImageFolder

    private void chooseSoundFile(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseSoundFile
        if (soundFileChooser == null) {
            soundFileChooser = new JFileChooser();
            if (soundFileField.getText().length() > 0) {
                soundFileChooser.setSelectedFile(new File(soundFileField.getText()));
            }
        }
        if (JFileChooser.APPROVE_OPTION == soundFileChooser.showOpenDialog(this)) {
            soundFileField.setText(soundFileChooser.getSelectedFile().getPath());
        }
    }//GEN-LAST:event_chooseSoundFile

    private void createMovie(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_createMovie
        // ---------------------------------
        // Check input
        // ---------------------------------
        final File soundFile = soundFileField.getText().trim().length() == 0 ? null : new File(soundFileField.getText().trim());
        final File imageFolder = imageFolderField.getText().trim().length() == 0 ? null : new File(imageFolderField.getText().trim());

        if (soundFile == null && imageFolder == null) {
            JOptionPane.showMessageDialog(this, "<html>You need to specify a folder with<br>image files and/or a sound file.");
            return;
        }

        final int width, height;
        final double fps;
        try {
            width = Integer.parseInt(widthField.getText());
            height = Integer.parseInt(heightField.getText());
            fps = Double.parseDouble(fpsField.getText());
        } catch (Throwable t) {
            JOptionPane.showMessageDialog(this, "<html>Width, Height and FPS must be numeric.");
            return;
        }
        if (width < 1 || height < 1 || fps < 1) {
            JOptionPane.showMessageDialog(this, "<html>Width, Height and FPS must be greater than zero.");
            return;
        }

        final QuickTimeWriter.VideoEncoding videoFormat;
        switch (encodingBox.getSelectedIndex()) {
            case 0:
                videoFormat = QuickTimeWriter.VideoEncoding.JPG;
                break;
            case 1:
            default:
                videoFormat = QuickTimeWriter.VideoEncoding.PNG;
                break;
            case 2:
                videoFormat = QuickTimeWriter.VideoEncoding.RAW;
                break;
        }

        // ---------------------------------
        // Update Preferences
        // ---------------------------------
        prefs.put("movie.imageFolder", imageFolderField.getText());
        prefs.put("movie.soundFile", soundFileField.getText());
        prefs.putInt("movie.width", width);
        prefs.putInt("movie.height", height);
        prefs.putDouble("movie.fps", fps);
        prefs.putInt("movie.encoding", encodingBox.getSelectedIndex());
        prefs.putBoolean("movie.passThrough", passThroughCheckBox.isSelected());


        // ---------------------------------
        // Choose an output file
        // ---------------------------------
        if (movieFileChooser == null) {
            movieFileChooser = new JFileChooser();
            if (prefs.get("movie.outputFile", null) != null) {
                movieFileChooser.setSelectedFile(new File(prefs.get("movie.outputFile", null)));
            }
        }
        if (JFileChooser.APPROVE_OPTION != movieFileChooser.showSaveDialog(this)) {
            return;
        }

        final File movieFile = movieFileChooser.getSelectedFile().getPath().toLowerCase().endsWith(".mov")//
                ? movieFileChooser.getSelectedFile()
                : new File(movieFileChooser.getSelectedFile().getPath() + ".mov");
        prefs.put("movie.outputFile", movieFile.getPath());
        createMovieButton.setEnabled(false);

        final boolean passThrough = passThroughCheckBox.isSelected();

        // ---------------------------------
        // Create the QuickTime movie
        // ---------------------------------
        SwingWorker w = new SwingWorker() {

            @Override
            protected Object doInBackground() {
                try {
                    // Read image files
                    File[] imgFiles = null;
                    if (imageFolder != null) {
                        imgFiles = imageFolder.listFiles(new FileFilter() {

                            @Override
                            public boolean accept(File f) {
                                return f.isFile() && !f.isHidden();
                            }
                        });
                        Arrays.sort(imgFiles);
                    }

                    // Check on first image, if we can actually do pass through
                    if (passThrough) {
                    }

                    // Delete movie file if it already exists.
                    if (movieFile.exists()) {
                        movieFile.delete();
                    }

                    if (imageFolder != null && soundFile != null) {
                        writeVideoAndAudio(movieFile, imgFiles, soundFile, width, height, fps, videoFormat, passThrough);
                    } else if (imageFolder != null) {
                        writeVideoOnly(movieFile, imgFiles, width, height, fps, videoFormat, passThrough);
                    } else {
                        writeAudioOnly(movieFile, soundFile);

                    }
                    return null;
                } catch (Throwable t) {
                    return t;
                }
            }

            @Override
            protected void done() {
                Object o;
                try {
                    o = get();
                } catch (Exception ex) {
                    o = ex;
                }
                if (o instanceof Throwable) {
                    Throwable t = (Throwable) o;
                    t.printStackTrace();
                    JOptionPane.showMessageDialog(QuickTimeMovieMakerMain.this, "<html>Creating the QuickTime Movie failed.<br>" + (t.getMessage() == null ? t.toString() : t.getMessage()), "Sorry", JOptionPane.ERROR_MESSAGE);
                }
                createMovieButton.setEnabled(true);
            }
        };
        w.execute();


    }//GEN-LAST:event_createMovie

    private void writeVideoOnly(File movieFile, File[] imgFiles, int width, int height, double fps, QuickTimeWriter.VideoEncoding videoFormat, boolean passThrough) throws IOException {
        ProgressMonitor p = new ProgressMonitor(QuickTimeMovieMakerMain.this, "Creating " + movieFile.getName(), "Creating Output File...", 0, imgFiles.length);
        Graphics2D g = null;
        BufferedImage imgBuffer = null;
        QuickTimeWriter qtOut = null;
        try {
            int timeScale = (int) (fps * 100.0);
            int duration = 100;

            qtOut = new QuickTimeWriter(movieFile);
            qtOut.addVideoTrack(videoFormat, timeScale, width, height);

            if (!passThrough) {
                imgBuffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
                g = imgBuffer.createGraphics();
                g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            }
            for (int i = 0; i < imgFiles.length && !p.isCanceled(); i++) {
                File f = imgFiles[i];
                p.setNote("Processing " + f.getName());
                p.setProgress(i);

                if (passThrough) {
                    qtOut.writeSample(0, f, duration);
                } else {
                    BufferedImage fImg = ImageIO.read(f);
                    g.drawImage(fImg, 0, 0, width, height, null);
                    qtOut.writeFrame(0, imgBuffer, duration);
                }

            }
            qtOut.close();
            qtOut = null;
        } finally {
            p.close();
            if (g != null) {
                g.dispose();
            }
            if (imgBuffer != null) {
                imgBuffer.flush();
            }
            if (qtOut != null) {
                qtOut.close();
            }
        }
    }

    private void writeAudioOnly(File movieFile, File audioFile) throws IOException {
        int length = (int) Math.min(Integer.MAX_VALUE, audioFile.length()); // file length is used for a rough progress estimate. This will only work for uncompressed audio.
        ProgressMonitor p = new ProgressMonitor(QuickTimeMovieMakerMain.this, "Creating " + movieFile.getName(), "Initializing...", 0, length);
        AudioInputStream audioIn = null;
        QuickTimeWriter qtOut = null;
        try {
            qtOut = new QuickTimeWriter(movieFile);

            audioIn = AudioSystem.getAudioInputStream(audioFile);
            AudioFormat audioFormat = audioIn.getFormat();
            qtOut.addAudioTrack(audioFormat);

            float audioSampleRate = audioFormat.getSampleRate();
            int audioFrameSize = audioFormat.getFrameSize();
            int audioFramesPerChunk = Math.max(1, 1024 / audioFrameSize);
            long count = 0;
            byte[] chunk = new byte[audioFrameSize * audioFramesPerChunk];
            for (int bytesRead = audioIn.read(chunk); bytesRead != -1; bytesRead = audioIn.read(chunk)) {
                qtOut.writeSamples(0, bytesRead / audioFrameSize, chunk, 0, bytesRead, 1);
                count += bytesRead;
                p.setProgress((int) count);
            }
            audioIn.close();
            audioIn = null;
            qtOut.close();
            qtOut = null;
        } catch (UnsupportedAudioFileException e) {
            IOException ioe = new IOException(e.getMessage());
            ioe.initCause(e);
            throw ioe;
        } finally {
            p.close();
            if (audioIn != null) {
                audioIn.close();
            }
            if (qtOut != null) {
                qtOut.close();
            }
        }
    }

    private void writeVideoAndAudio(File movieFile, File[] imgFiles, File audioFile, int width, int height, double fps, QuickTimeWriter.VideoEncoding videoFormat, boolean passThrough) throws IOException {
        ProgressMonitor p = new ProgressMonitor(QuickTimeMovieMakerMain.this, "Creating " + movieFile.getName(), "Creating Output File...", 0, imgFiles.length);
        AudioInputStream audioIn = null;
        QuickTimeWriter w = null;
        BufferedImage imgBuffer = null;
        Graphics2D g = null;
        try {
            audioIn = AudioSystem.getAudioInputStream(audioFile);
            AudioFormat audioFormat = audioIn.getFormat();

            // duration of a single sample
            int asduration = 1;
            int vsduration = 100;

            // create writer
            w = new QuickTimeWriter(movieFile);
            w.addAudioTrack(audioFormat); // audio in track 0
            w.addVideoTrack(QuickTimeWriter.VideoEncoding.PNG, (int) (fps * vsduration), width, height);  // video in track 1

            // calculate total movie duration in media time units for each track
            long atmax = (long) w.getTimeScale(0) * imgFiles.length * vsduration / w.getTimeScale(1);
            long vtmax = imgFiles.length * vsduration;

            // half a second in media time units (we interleave twice per second)
            long atstep = w.getTimeScale(0) / 2;
            long vtstep = w.getTimeScale(1) / 2;

            // the time when the next interleave occurs in media time units
            long atnext = w.getTimeScale(0); // offset audio by 1 second
            long vtnext = 0;

            // the current time in media time units
            long atime = 0;
            long vtime = 0;

            // create buffers
            int asamplesize = 2 * 2; // 16-bit stereo * 2 channels
            byte[] audio = new byte[(int) (atstep * asamplesize)];
            if (!passThrough) {
                imgBuffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
                g = imgBuffer.createGraphics();
                g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            }
            // main loop
            while ((atime < atmax || vtime < vtmax) && !p.isCanceled()) {
                atnext = Math.min(atmax, atnext + atstep); // advance audio to next interleave time
                while (atime < atnext) { // catch up with audio time
                    int duration = (int) Math.min(audio.length / asamplesize, atmax - atime);
                    p.setNote("Processing " + audioFile.getName() + " " + atime);
                    int bytesRead = audioIn.read(audio, 0, (int) duration * asamplesize);
                    if (bytesRead > 0) {
                        w.writeSamples(0, bytesRead / asamplesize, audio, 0, bytesRead, asduration);
                        atime += duration;
                    } else {
                        break;
                    }
                }
                vtnext = Math.min(vtmax, vtnext + vtstep); // advance video to next interleave time
                while (vtime < vtnext) { // catch up with video time
                    p.setProgress((int) vtime / vsduration);
                    int duration = (int) Math.min(vsduration, vtmax - vtime);
                    p.setNote("Processing " + imgFiles[(int) vtime / vsduration].getName());
                    if (passThrough) {
                        w.writeSample(1, imgFiles[(int) vtime / 100], vsduration);
                    } else {
                        BufferedImage fImg = ImageIO.read(imgFiles[(int) vtime / 100]);
                        g.drawImage(fImg, 0, 0, width, height, null);
                        fImg.flush();
                        w.writeFrame(1, imgBuffer, vsduration);
                    }
                    vtime += duration;
                }
            }
            w.close();
            w = null;
        } catch (UnsupportedAudioFileException e) {
            IOException ioe = new IOException(e.getMessage());
            ioe.initCause(e);
            throw ioe;
        } finally {
            p.close();
            if (w != null) {
                w.close();
            }
            if (audioIn != null) {
                audioIn.close();
            }
            if (g != null) {
                g.dispose();
            }
            if (imgBuffer != null) {
                imgBuffer.flush();
            }
        }
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                QuickTimeMovieMakerMain m = new QuickTimeMovieMakerMain();
                m.setVisible(true);
                m.pack();
            }
        });
    }
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JLabel aboutLabel;
    private javax.swing.JButton chooseImageFolderButton;
    private javax.swing.JButton chooseSoundFileButton;
    private javax.swing.JButton createMovieButton;
    private javax.swing.JComboBox encodingBox;
    private javax.swing.JLabel encodingLabel;
    private javax.swing.JTextField fpsField;
    private javax.swing.JLabel fpsLabel;
    private javax.swing.JTextField heightField;
    private javax.swing.JLabel heightLabel;
    private javax.swing.JTextField imageFolderField;
    private javax.swing.JLabel imageFolderHelpLabel;
    private javax.swing.JCheckBox passThroughCheckBox;
    private javax.swing.JTextField soundFileField;
    private javax.swing.JLabel soundFileHelpLable;
    private javax.swing.JTextField widthField;
    private javax.swing.JLabel widthLabel;
    // End of variables declaration//GEN-END:variables
}
